package info.jodel.propertiesmanager;

import info.jodel.propertiesmanager.annotations.PropertyValue;
import info.jodel.propertiesmanager.exceptions.PropertySetException;
import info.jodel.propertiesmanager.util.ClassUtil;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class for managing properties
 */
public class PropertiesManager {
	/**
	 * Delimeter for String-Arrays
	 */
	private static final String STRING_ARRAY_DELIMETER = ";";

	/**
	 * Logger for this class
	 */
	private static final Logger logger = LoggerFactory
			.getLogger(PropertiesManager.class);

	/**
	 * Single instance of property holder
	 */
	static private PropertiesManager propertyHolder = new PropertiesManager();

	/**
	 * 
	 * @return
	 */
	static public PropertiesManager getPropertyHolder() {
		return PropertiesManager.propertyHolder;
	}

	/**
	 * Properties-object for all properties-files
	 */
	private Properties properties = new Properties();

	/**
	 * Add properties-file to PropertyHolder
	 * 
	 * @param inputStream
	 *            stream for properties-file
	 * @throws FileNotFoundException
	 *             error reading file
	 * @throws IOException
	 *             error reading file
	 */
	public void addPropertyFile(InputStream inputStream)
			throws FileNotFoundException, IOException {
		if (logger.isDebugEnabled()) {
			logger.debug("addPropertyFile(File) - start"); //$NON-NLS-1$
		}

		// readout file
		Properties properties = new Properties();
		properties.load(inputStream);

		// add all properties to properties-object
		this.properties.putAll(properties);

		if (logger.isDebugEnabled()) {
			logger.debug("addPropertyFile(File) - end"); //$NON-NLS-1$
		}
	}

	/**
	 * Private constructor for single instation
	 */
	private PropertiesManager() {
		// nothing to do..
	}

	/**
	 * Set all values for field, which have the PropertyValue-Annotation
	 * 
	 * @param object
	 *            object to set property-vaklues for
	 */
	public void setProperties(Object object) throws PropertySetException {
		if (logger.isDebugEnabled()) {
			logger.debug("setProperties(Object) - start"); //$NON-NLS-1$
		}

		// get all field, which are annotated with PropertyValue-Annotation
		Field[] annotatedFields = ClassUtil.getAnnotatedDeclaredFields(
				object.getClass(), PropertyValue.class, true);

		/*
		 * Iterate through all annotated fields
		 */
		for (Field field : annotatedFields) {
			if (logger.isDebugEnabled()) {
				logger.debug(
						"setProperties(Object) - Found annotated field - field={}, type={}", field.getName(), field.getType().getSimpleName()); //$NON-NLS-1$
			}

			String key = field.getAnnotation(PropertyValue.class).key();
			// check, if property exists
			if (false == this.properties.containsKey(key)) {
				logger.error(
						"setProperties(Object) - Could not assign filed, did not found key in properties-field - field={}, key={}", field.getName(), key); //$NON-NLS-1$ //$NON-NLS-2$

				throw new PropertySetException(
						"Could not assign filed, did not found key in properties-field"
								+ field.getName() + "for key: " + key);
			}

			// Save original value of accessible
			boolean accessable = field.isAccessible();

			// if field is not accessible, change it
			if (false == accessable) {
				field.setAccessible(true);
			}

			// get type of field
			Class<?> fieldType = field.getType();
			// get property-string-value
			String stringValue = this.properties.getProperty(key);

			/*
			 * Check, which type is to set
			 */
			try {

				// String-Type
				if (fieldType.equals(String.class)) {
					// save value
					field.set(object, stringValue);
				}
				// String-Array-Type
				if (fieldType.equals(String[].class)) {
					// save value
					field.set(object, stringValue.split(STRING_ARRAY_DELIMETER));
				}
				// BigInteger
				else if (fieldType.equals(BigInteger.class)) {
					// parste it
					BigInteger bigInteger = new BigInteger(stringValue);
					// save value
					field.set(object, bigInteger);
				}
				// Byte
				else if (fieldType.equals(Byte.class)
						|| fieldType.equals(Byte.TYPE)) {
					// Create temporary object from string
					Byte tmp = new Byte(stringValue);
					// check ,if field is primitive
					field.set(object, tmp);
				}
				// Double
				else if (fieldType.equals(Double.class)
						|| fieldType.equals(Double.TYPE)) {
					Double tmp = new Double(stringValue);
					// field.setInt(object, tmp);
					field.set(object, tmp);
				}
				// Float
				else if (fieldType.equals(Float.class)
						|| fieldType.equals(Float.TYPE)) {
					Float tmp = new Float(stringValue);
					// field.setInt(object, tmp);
					field.set(object, tmp);

				}
				// Integer
				else if (fieldType.equals(Integer.class)
						|| fieldType.equals(Integer.TYPE)) {
					Integer tmp = new Integer(stringValue);
					// field.setInt(object, tmp);
					field.set(object, tmp);
				}
				// Long
				else if (fieldType.equals(Long.class)
						|| fieldType.equals(Long.TYPE)) {
					Long tmp = new Long(stringValue);
					// field.setInt(object, tmp);
					field.set(object, tmp);
				}
				// Short
				else if (fieldType.equals(Short.class)
						|| fieldType.equals(Short.TYPE)) {
					Short tmp = new Short(stringValue);
					// field.setInt(object, tmp);
					field.set(object, tmp);
				} else {
					// Wrong type
					throw new PropertySetException("Wrong type: "
							+ fieldType.getCanonicalName());
				}

				// save further state of accessible
				if (false == accessable) {
					field.setAccessible(accessable);
				}

			} catch (Exception e) {
				logger.warn("setProperties(Object) - exception ignored", e); //$NON-NLS-1$
				// Throw exception to higher instance
				throw new PropertySetException(e);
				// TODO there is something wrong..
			}
		}

		if (logger.isDebugEnabled()) {
			logger.debug("setProperties(Object) - end"); //$NON-NLS-1$
		}
	}
}
